import os
import chardet
import time
import docx
import re
import json
import uuid
import threading
from typing import List, Dict, Tuple
from flask import Flask, request, jsonify
from openai import OpenAI
from collections import Counter
from flask_cors import CORS
from fuzzywuzzy import fuzz

app = Flask(__name__)
CORS(app, supports_credentials=True, origins=["*"])


class ExamBotManager:
    def __init__(self):
        self.lock = threading.Lock()
        self.bots = {}

    def get_bot(self, session_id):
        with self.lock:
            return self.bots.get(session_id)

    def create_bot(self, session_id, config):
        with self.lock:
            bot = SmartExamBot(
                api_key=config["api_key"],
                base_url=config["base_url"],
                max_questions=config["max_questions"]
            )
            self.bots[session_id] = bot
            return bot


exam_bot_manager = ExamBotManager()


class SmartExamBot:
    def __init__(self, api_key: str, base_url: str, max_questions: int = 5):
        self.api_key = api_key
        self.base_url = base_url
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.question_count = 0
        self.total_score = 0
        self.max_possible_score = 0
        self.max_questions = max_questions
        self.question_scores = []
        self.cannot_answer_patterns = re.compile(
            r'不知道|不清楚|没学过|不懂|不了解|不会做|不知道怎么答|无法回答|我举不出来例子|分析不出来',
            re.IGNORECASE
        )
        self.irrelevant_patterns = re.compile(
            r'跑题|偏题|无关|答非所问|换个题|另一个|不相关|说点别的|其他内容|别的话题',
            re.IGNORECASE
        )
        self.image_patterns = re.compile(
            r'如图|图示|图片|图表|图[.\d]+|figure|graph|chart|diagram|illustration',
            re.IGNORECASE
        )
        self.knowledge_content = ""
        self.keywords = []
        self.current_question = ""
        self.question_dimensions = []
        self.messages = []
        self.unrelated_topics = {'天气', '吃饭', '电影', '游戏', '睡觉', '时间', '日期',
                                 '音乐', '歌曲', '明星', '电影', '旅游', '购物', '美食',
                                 '体育', '运动', '小说', '书籍', '电视剧', '综艺'}

    def _extract_keywords(self, text: str) -> List[str]:
        words = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', text)
        word_counts = Counter(words)
        stop_words = {'可以', '一个', '这个', '我们', '进行', '需要', '应该', '问题', '方法'}
        keywords = [word for word, count in word_counts.most_common(20)
                    if word not in stop_words and count > 1]
        return keywords[:10] if keywords else ["数据", "分析", "技术"]

    def _extract_dimensions(self, question: str) -> List[str]:
        match = re.search(r'请从(.*?)分析', question)
        if match:
            dimensions_text = match.group(1)
            dimensions = [dim.strip() for dim in re.split(r'、|和|与', dimensions_text) if dim.strip()]
            return dimensions
        return []

    def detect_encoding(self, file_path: str) -> str:
        if file_path.lower().endswith('.docx'):
            return 'docx'
        try:
            with open(file_path, 'rb') as file:
                result = chardet.detect(file.read(10240))
                return result['encoding'] or 'utf-8'
        except Exception as e:
            raise Exception(f"编码检测失败: {str(e)}")

    def read_file_content(self, file_path: str) -> str:
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")

        if not os.path.isfile(file_path):
            raise IsADirectoryError(f"{file_path}不是一个文件")

        try:
            encoding = self.detect_encoding(file_path)

            if encoding == 'docx':
                doc = docx.Document(file_path)
                full_text = []
                for para in doc.paragraphs:
                    full_text.append(para.text)
                for table in doc.tables:
                    for row in table.rows:
                        row_text = [cell.text for cell in row.cells]
                        full_text.append('\t'.join(row_text))
                return '\n'.join(full_text)

            encodings_to_try = [encoding, 'utf-8', 'gbk', 'latin-1']
            for enc in encodings_to_try:
                try:
                    with open(file_path, 'r', encoding=enc, errors='replace') as file:
                        return file.read()
                except UnicodeDecodeError:
                    continue
            raise Exception("无法识别文件编码")
        except Exception as e:
            raise Exception(f"文件读取失败: {str(e)}")

    def get_ai_response(self, messages: List[Dict], max_retries: int = 3) -> str:
        retry_count = 0
        while retry_count < max_retries:
            try:
                response = self.client.chat.completions.create(
                    model="deepseek-v3",
                    messages=messages,
                    temperature=0.2,
                    timeout=30
                )
                return response.choices[0].message.content
            except Exception as e:
                retry_count += 1
                if retry_count >= max_retries:
                    raise Exception(f"API请求失败: {str(e)}")
                time.sleep(2)

    def is_cannot_answer(self, user_input: str) -> bool:
        user_input = user_input.strip()
        if not user_input or re.fullmatch(r'[\s,.!?;]+', user_input):
            return True
        return self.cannot_answer_patterns.search(user_input) is not None

    def is_irrelevant(self, user_answer: str) -> Tuple[bool, str]:
        user_answer = user_answer.strip().lower()
        if self.irrelevant_patterns.search(user_answer):
            return True, "回答明确表示与题目无关"

        question_keywords = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', self.current_question)
        all_keywords = question_keywords + self.keywords + self.question_dimensions
        has_relevant_keywords = any(keyword in user_answer for keyword in all_keywords)

        if not has_relevant_keywords:
            for topic in self.unrelated_topics:
                if topic in user_answer:
                    return True, f"回答涉及无关主题({topic})，视为乱答"
            if len(user_answer) < 5:
                return True, "回答过短且不涉及题目相关内容，视为乱答"
            return True, "回答与题目及知识库内容无关，视为乱答"
        return False, ""

    def is_copying_question(self, user_answer: str) -> bool:
        def clean_text(text):
            text = re.sub(r'[^\w\u4e00-\u9fa5]', '', text)
            return text.lower().strip()

        cleaned_question = clean_text(self.current_question)
        cleaned_answer = clean_text(user_answer)

        if len(cleaned_answer) == 0:
            return False

        similarity_ratio = fuzz.ratio(cleaned_question[:100], cleaned_answer[:100]) / 100
        is_contain_full_question = cleaned_question in cleaned_answer

        return (similarity_ratio > 0.8 or
                is_contain_full_question or
                (len(cleaned_answer) <= len(cleaned_question) * 1.2 and similarity_ratio > 0.6))

    def _generate_reference_answer(self) -> str:
        """生成当前题目的参考答案"""
        prompt = f"""
        请根据题目和知识库生成标准答案：
        题目: {self.current_question}
        要求维度: {', '.join(self.question_dimensions)}
        知识库内容: {self.knowledge_content[:3000]}

        要求：
        1. 覆盖所有维度
        2. 引用知识库关键内容
        3. 结构清晰（分点列出）
        4. 不要包含评分信息
        """
        return self.get_ai_response([{"role": "user", "content": prompt}])

    def _compare_with_reference(self, user_answer: str, reference_answer: str) -> Tuple[int, str]:
        """将用户答案与参考答案对比评分"""
        prompt = f"""
        请按以下规则评分：
        - 题目: {self.current_question}
        - 要求维度: {', '.join(self.question_dimensions)}
        - 参考答案: {reference_answer}
        - 用户答案: {user_answer}

        评分标准（严格对比）：
        10分 — 完全覆盖所有维度，内容与参考答案高度一致
        8-9分 — 覆盖主要维度，关键点与参考答案基本一致
        6-7分 — 部分覆盖维度，有少量一致内容
        4-5分 — 仅提及基本概念，无深入分析
        0-3分 — 与参考答案无关

        请返回格式：分数|详细对比分析
        示例：8|用户覆盖了A、B维度，但在C维度缺少具体案例
        """

        response = self.get_ai_response([{"role": "user", "content": prompt}])
        try:
            score, analysis = response.split("|", 1)
            return int(score.strip()), analysis.strip()
        except:
            return 6, "评分解析失败，按基准分处理"

    def generate_question(self):
        """生成题目（不包含任何评分标准）"""
        if self.question_count >= self.max_questions:
            return None

        prompt = f"""
        请基于以下知识库出一道主观题：
        {self.knowledge_content[:3000]}

        要求：
        1. 必须包含关键词: {', '.join(self.keywords)}
        2. 明确要求从多个维度回答（如"请从A、B、C分析..."）
        3. 题目清晰，不包含示例、评分标准或其他无关内容
        4. 禁止出现"如图"、"图表"等依赖图片的描述
        """

        question = self.get_ai_response([{"role": "user", "content": prompt}])
        question = re.sub(r'评分标准.*|\n\d+分.*', '', question)  # 移除评分相关文本
        question = question.split("|")[0].strip()

        if question:
            self.question_count += 1
            self.current_question = question
            self.question_dimensions = self._extract_dimensions(question)
            self.max_possible_score += 10
            return question
        return None

    def evaluate_answer(self, user_answer: str):
        """评估用户答案"""
        # 1. 检查无效回答（0分情况）
        if self.is_invalid_answer(user_answer):
            return self._handle_invalid_answer(user_answer)

        # 2. 生成参考答案并对比评分
        reference_answer = self._generate_reference_answer()
        score, feedback = self._compare_with_reference(user_answer, reference_answer)

        # 3. 记录分数
        self.total_score += score
        self.question_scores.append(score)

        return {
            "score": score,
            "feedback": feedback,
            "reference_answer": reference_answer,
            "need_follow_up": False
        }

    def is_invalid_answer(self, answer: str) -> bool:
        """检测无效回答（复制题目、乱答、不会答等）"""
        return (self.is_copying_question(answer) or
                self.is_cannot_answer(answer) or
                self.is_irrelevant(answer)[0])

    def _handle_invalid_answer(self, answer: str) -> Dict:
        """处理无效回答（返回0分结果）"""
        self.total_score += 0
        self.question_scores.append(0)

        if self.is_copying_question(answer):
            feedback = "检测到直接复制题目内容的行为，本题得0分"
        elif self.is_cannot_answer(answer):
            feedback = "检测到无法回答的表述，本题得0分"
        else:
            _, feedback = self.is_irrelevant(answer)
            feedback = f"检测到无关回答: {feedback}，本题得0分"

        return {
            "score": 0,
            "feedback": feedback,
            "reference_answer": "",
            "need_follow_up": False
        }

    def init_exam(self, knowledge_content: str):
        """初始化考试"""
        self.knowledge_content = knowledge_content
        self.keywords = self._extract_keywords(knowledge_content)
        self.messages = [
            {"role": "system", "content": "你是一个考试机器人，负责出题和评分"},
            {"role": "assistant", "content": "考试准备就绪，请输入'开始'进行第一题。"}
        ]


# Flask路由保持不变（与之前相同）
@app.route('/')
def index():
    with open('test.html', 'r', encoding='utf-8') as f:
        return f.read()


@app.route('/start_exam', methods=['POST'])
def start_exam():
    session_id = request.cookies.get('session_id') or str(uuid.uuid4())
    CONFIG = {
        "api_key": "sk-43f6c93de4ff47ba9cbd7e0dc9ed68b7",
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "file_path": r"E:/第3章 数据获取.docx",
        "max_questions": 5
    }

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        bot = exam_bot_manager.create_bot(session_id, CONFIG)

    try:
        knowledge_content = bot.read_file_content(CONFIG["file_path"])
        bot.init_exam(knowledge_content)
        question = bot.generate_question()

        if question:
            response = jsonify({
                'question': question,
                'question_count': bot.question_count,
                'max_questions': bot.max_questions
            })
            response.set_cookie('session_id', session_id, httponly=True, samesite='None', secure=True)
            return response
        return jsonify({'error': '题目生成失败'}), 500
    except Exception as e:
        return jsonify({'error': f'考试启动失败: {str(e)}'}), 500


@app.route('/submit_answer', methods=['POST'])
def submit_answer():
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    data = request.json
    result = bot.evaluate_answer(data.get('answer', ''))
    result.update({
        'question_count': bot.question_count,
        'current_question': bot.question_count,
        'total_score': bot.total_score,
        'max_possible_score': bot.max_possible_score
    })
    return jsonify(result)


@app.route('/next_question', methods=['GET'])
def next_question():
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    try:
        if bot.question_count < bot.max_questions:
            question = bot.generate_question()
            if question:
                return jsonify({
                    'question': question,
                    'question_count': bot.question_count
                })
            return jsonify({'error': '题目生成失败'}), 500
        return jsonify({
            'message': '考试已完成',
            'total_score': bot.total_score,
            'average_score': bot.total_score / bot.max_questions,
            'question_scores': bot.question_scores
        })
    except Exception as e:
        return jsonify({'error': f'获取题目失败: {str(e)}'}), 500


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)